"""
Ядро Чио собранное в одном файле.

- Colors  -- цвета для терминала
- Logger  -- логгирование в терминале
- Ui      -- украшательства в терминале
- Plugins -- класс плагина
- Event   -- класс события
- Context -- вспомогательный класс вызываемых функций
- Chio    -- гоавный класс бота

Author: Milinuri Nirvalen
Ver: 2.3
"""

import asyncio         # <-- для запуска
import shlex           # <-- для разбора args
import time            # <-- для анализа скорости разбора
import os              # <-- работа с файлами
import importlib.util  # <-- импорт модулей
import datetime        # <-- работа с временем
import shutil          # <-- размер терминала
import json            # <-- работа с конфигами
import re              # <-- регулярные выражения


def fcheck(path, create=False):
    '''проверяет существование папки
    : param path   :  путь до папки
    : param create :  создать ли папку при её отцуцтвии'''

    l = Logger('check folder')
    if not os.path.exists(path):
        if create:
            os.mkdir(path)
            l.log(f'создание директории {path}', 'c')
        else:
            l.log(f'дирректория {path} не найдена', 'w')
    else:
        return True

def count_files(folder, count=0):
    for name in os.listdir(folder):
        path = os.path.join(folder, name)

        if os.path.isdir(path) and not name.startswith('_') and name not in ['chiori']:
            count = count_files(path, count)

        elif re.match(r"^[^_].*\.py$", name):
            count += 1
    return count


################################################################################
# Config <-- файлы с данными
################################################################################

class Config(object):
    """docstring for Config"""
    def __init__(self, name=None, model=None, filepath=None):
        super(Config, self).__init__()
        self.name = name
        self.model = model

        self.file_data = {}
        self.group_data = {} or self.model
        self.filepath = filepath or 'config.toml'
        self.l = Logger(f'config: {self.name or self.filepath}')
        self.lock = False

        if not self.name and not filepath and self.model:
            self.lock = True
            self.l.log('заблокировано', 'w')

        if not self.lock:
            self.load()

    def save(self):
        if self.name:
            self.file_data[self.name] = self.group_data or self.model
        elif self.model and not self.file_data:
            self.file_data = self.model

        with open(self.filepath, 'w') as f:
            toml.dump(self.file_data, f)
        self.l.log('сохранено!', 'c')

    def load(self):
        """загрузка данных из toml файла"""
        if os.path.exists(self.filepath):
            self.file_data = toml.load(self.filepath)

            if self.name:
                if self.name in self.file_data:
                    self.group_data = self.file_data[self.name]
                else:
                    self.l.log('группа данных не найдена', 'e')
                    self.save()

        else:
            self.l.log('файл не найден', 'e')
            self.save()

def import_module(name, path):
    """Import module from specified path with specified name.

    : param name :  module's name
    : param path :  path to module's file

    :return: imported module"""

    spec = importlib.util.spec_from_file_location(name, os.path.abspath(path))
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)

    return module

def load_plugins_from_file(app, path):
    l = Logger(path)
    try:
        mod = import_module(path, path)
    except Exception as e:
        print()
        l.log(e, 'e')
        return f'{path} -> {e}'
    else:
        journal = []
        for pl in [getattr(mod, "p", None),
                   getattr(mod, "plugin", None),
                   *getattr(mod, "plugins", ())]:
            if isinstance(pl, Plugin):
                if pl.__dict__.get('disabled'):
                    journal.append(f'{path} -> disabled')
                    continue

                target_backend = pl.__dict__.get('backend', '*')
                if target_backend == '*' or target_backend == app.backend.name:
                    name = path.split('/')[-1]
                    pl.path = path.split(name)[0]
                    app.plugins.append(pl)

                    for x in pl._on_init:
                        app.add_event_handler(x)
                    for x in pl._before_commands:
                        app.add_event_handler(x, 'before')
                    for x in pl._after_commands:
                        app.add_event_handler(x, 'after')

                    for k, v in pl._handlers.items():
                        app.handlers[k] = {'func':v['func'], 'level':v['level']}

                    app.commands += pl.commands
        return journal

def load_plugins(app, folder, main_folder=True, count=0, files=0, journal=[]):
    u = Ui('plugin loader')
    if main_folder:
        fcheck(folder, True)
        files = count_files(folder)
        u.l.log(f'загрузка плагинов из: {folder}')

    for name in os.listdir(folder):
        path = os.path.join(folder, name)

        if os.path.isdir(path) and not name.startswith('_'):
            count, journal = load_plugins(app, path, False, count, files)

        elif re.match(r"^[^_].*\.py$", name) and name not in app.ignore_files:
            count += 1
            resp = load_plugins_from_file(app, path)
            if resp:
                journal.append(resp)
            u.progress_bar(count, files, printEnd='\r')
    if count == files and main_folder:
        print()
    return count, journal


################################################################################
# Colors <-- цвета для терминале
################################################################################

class Colors():
    def __init__(self):
        super(Colors, self).__init__()
        # словарь цыетоы
        self.colors = {'black':'\033[30m', 'red':'\033[31m',
                       'green':'\033[32m', 'yellow':'\033[33m',
                       'blue':'\033[34m','magenta':'\033[35m',
                       'cyan':'\033[36m', 'white':'\033[37m',

                       'gray':'\033[90m', 'lred':'\033[91m',
                       'lgreen':'\033[92m', 'lyellow':'\033[93m',
                       'lblue':'\033[94m', 'lmagenta':'\033[95m',
                       'lcyan':'\033[96m', 'lwhite':'\033[97m'}

        # добавляем словарь цветов в атрибуты класса
        self.reset = '\033[0m'
        self.__dict__.update(self.colors)

    def colortest(self):
        # выводит все цвета класса Colors

        for k, v in self.colors.items():
            print(v + k, end=' ')
        print(self.reset+'|')

################################################################################
# Logger <-- логгирование в терминале
################################################################################

class Logger(Colors):
    """Логгирование в терминале, запись логов в файл"""
    def __init__(self, name):
        super(Logger, self).__init__()
        self.name = name

    def set_group(self, text):
        '''заменяет специальные метки, на значки логов
        : param text :  текст, в котором нужно преобразовать группы

        : return : преобразованный текст'''
        groups = {'<i>':f'{self.lyellow}[=]{self.reset}',
     '<c>':f'{self.lyellow}[+]{self.reset}',
     '<w>':f'{self.lyellow}[!]{self.reset}',
     '<e>':f'{self.lyellow}[-]{self.reset}'}

        for par, arg in groups.items():
            text = text.replace(par, arg)
        return text

    def date(self):
        t = datetime.datetime.now().strftime('%H:%M:%S|')
        return self.gray+t+self.reset

    def log(self, text, log_type=''):
        if log_type == 'i':
            group = f'{self.reset}[=] {self.gray}{self.name}{self.reset}'
        elif log_type == 'c':
            group = f'{self.green}[+] {self.gray}{self.name}{self.lgreen}'
        elif log_type == 'w':
            group = f'{self.lyellow}[!] {self.yellow}{self.name}{self.lyellow}'
        elif log_type == 'e':
            group = f'{self.lyellow}[-] {self.yellow}{self.name}{self.lred}'
        elif log_type:
            group = f'{self.reset}{self.name} {self.gray}{log_type}{self.reset}'
        else:
            group = f'{self.gray}{self.name}{self.reset}'

        print(f'{self.date()} {self.set_group(group)} {text} {self.reset}')

    def logtest(self):
        # выводит все типы логов
        types = [None, 'i', 'c', 'w', 'e',
                'abc', 'abc <i>', 'abd <c>', 'abc <w>', 'abc <e>']
        for x in types:
            self.log('text', x)


################################################################################
# Ui <-- украшательства в терминале
################################################################################

class Ui(Colors):
    """docstring for Ui"""
    def __init__(self, name=None):
        super(Ui, self).__init__()
        self.l = Logger(name)
        self.name = name

        # настройки внешнего вида
        self.mc = '<gre>'
        self.bc = '<yel>'
        self.tc = '<mag>'

        # получаем размер терминала
        self.get_size()

    def colored(self, text):
        # преобразует ключевые слова в цветовые коды
        codes = {'<m>':self.mc, '<b>':self.bc, '<t>':self.tc, '<r>':self.reset}
        for k, v in codes.items():
            text = text.replace(k, v)

        for k, v in self.colors.items():
            text = text.replace(f'<{k[0:3]}>', v)

        text += self.reset
        return text

    def clear(self):
        # очищает терминал
        if os.name == 'nt':
            os.system('cls')
        else:
            os.system('clear')

    def get_size(self):
        # получение размера окна терминала и обновление этих данных
        s = shutil.get_terminal_size()
        self.size = [s[0], s[1]]

    def row(self, name=None):
        '''отрисовывает визуальный разделитель
        : param name :  имя для разделителя'''
        if not name:
            print(self.colored(f"<m>{'─'*self.size[0]}"))
        else:
            sep = round((self.size[0]-len(name)-3)/2)
            print(self.colored(
            f'<m>{"─"*sep}<b>[<t>{name}<b>]<m>{"─"*sep}'))

    def param(self, k, v=''):
        print(self.colored(f'<b>{k[0].upper()}{k[1:]}<r>: <t>{v}'))

    def dict_to_list(self, target):
        # преобразует словарь в список
        temp = []
        for k, v in target.items():
            temp.append(self.colored(f'<t>{k}<r>: {v}'))
        return temp

    def print_list(self, target, name=None):
        self.param('count', len(target))
        self.param(name or 'items')
        sep = 0

        # count
        for x in target:
            if len(x) > sep:
                sep = len(x)

        # render
        for u in range(len(target)):
            name = target[u]
            if u == (len(target)-1):
                print(self.colored(
                    f'<m>└───<b>[<t>{name}{" "*(sep-len(name))}<b>]'))
            else:
                print(self.colored(
                    f'<m>├───<b>[<t>{name}{" "*(sep-len(name))}<b>]'))

    def decor_arg(self, arg, decor= {'0': 'нет', '1': 'да'}):
        for k, v in decor.items():
            arg = str(arg).replace(str(k), str(v))
        return arg

    def progress_bar (self, iteration, total,
                      prefix = '',
                      suffix = '',
                      length = None,
                      sim_on = '▰',
                      printEnd = "\n",
                      hide=False,
                      colors=False):

        if not length:
            length = round(self.size[0] / 100 * 40)
        percent = round(100 * (iteration / total))

        filledLength = int(length * iteration // total)

        if colors:
            if float(percent) > 75:
                color = self.lcyan
            elif float(percent) > 50:
                color = self.lgreen
            elif float(percent) > 25:
                color = self.lyellow
            else:
                color = self.lred
        else:
            color = self.reset

        bar = color + sim_on*(filledLength) + self.reset + ' ' * (length - filledLength)
        text = f'\r{prefix} |{bar}| {iteration}/{total} ({percent}) {suffix}'
        if not hide:
            print(text, end= printEnd)
        return text

    def window(self, *lines, **params):
        # border
        border_type = params.get('border', 'standart')
        scheme = params.get('scheme', ['h', 'm', 'f'])
        indent = 0
        borders = {'standart':['╔', '╗', '╚', '╝', '═', '║'],
                   'retro': ['+', '+', '+', '+', '-', '|']}
        border = borders[border_type]

        # lenght window
        if 'lenght' in params:
            line = params['lenght']
        else:
            line = max([ len(x) for x in lines])
            if line % 2:
                line += 1

        if params.get('pos') == 'right':
            indent = u.size[0] - 2 - line

        # render
        for x in scheme:
            if x == 'h':
                print(' '*indent+border[0]+border[4]*line+border[1])
            elif x == 'f':
                print(' '*indent+border[2]+border[4]*line+border[3])
            elif x == 'm':
                for l in lines:
                    print(' '*indent+border[5]+l+' '*(line-len(l))+border[5])


################################################################################
# Plugin <-- класс плагина
################################################################################

class Plugin:
    def __init__(self, **kwargs):
        '''класс плагина
        : param **kwargs :  доплнительные параметры плагина'''

        # добавляем параметры к атрибутам
        self.__dict__.update(kwargs)
        self.l = Logger(self.__dict__.get('name', 'Plugin'))

        # локальное хранилище
        self._on_init = []
        self._before_commands = []
        self._after_commands = []
        self._handlers = {}
        self._triggers = []
        self.usage = {}
        self.commands = 0

    def eventHandler(self, *event_types):
        ''' декоратор обработчика событий
        : param *event_types :  типы событий'''
        def wrapper(func):
            if 'init' in event_types:
                self._on_init.append(func)
            elif 'before' in event_types:
                self._before_commands.append(func)
            elif 'after' in event_types:
                self._after_commands.append(func)

            return func
        return wrapper


    def command(self, *commands, **params):
        '''декоратор команды
        : param *commands :  перечень названий обработчиков
        : param **params  :  дополнительные параметры команды'''
        self.commands += 1

        # устанавливаем уровень полнимочий (локальный, глобальный)
        if params.get('level', 0):
            level = params['level']
        else:
            level = self.__dict__.get('level', 1)

        # дополняем атрибут usage для написания автоматических описаний
        self.usage[str(self.commands)] = {'cmd':', '.join(commands),
                                     'usage':params.get("usage"),
                                     'level':level}

        def wrapper(func):
            for c in commands:
                # добавляем обработчик и название обработчика
                self._triggers.append(c)
                self._handlers[c] = {'func':func, 'level':level}
            return func
        return wrapper

    def load(self, path):
        '''метод загрузки плагина
        пример: m = p.load("main.py") == import main
        : param path :  путь до импортируемово файла

        : return : модуль'''
        filepath = str(self.path)+ '/' + path

        spec = importlib.util.spec_from_file_location(filepath, os.path.abspath(filepath))
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)

        return module

    def log(self, text, log_type='i'):
        self.l.log(text, log_type)


################################################################################
# Event <-- класс события
################################################################################

class Event:
    __slots__ = ("model")

    def __init__(self, model=None):
        """класс события
        : param model :  dict своей модели события"""

        self.model = model or {
            'from':{
                'id':0,
                'is_mechanism':False,
                'first_name':'',
                'last_name':'',
                'username':'',
                'is_admin':False},
            'to':{
                'is_chat':False,
                'id':0,
                'is_admin':False},
            'id':0,
            'level':1,
            'attachs':[],
            'reply':{},
            'date':0,
            'text':'',
            'raw':None}

    def get(self, param):
        '''метод получения параметра модели
        : param param :  название переметра 1.2.3

        : returt :  например 1.2, значение 2, если не найдено, 1'''

        params = param.split('.')
        temp = self.model.get(params[0])

        for p in params[1:]:
            if type(temp) == dict:
                temp = temp.get(p)

        return temp

    def merge(self, target, model=None):
        '''метод совмещения словаря с self.model
        : param target :  словарь, который необходимо совместить'''
        if not model:
            model = self.model

        for k, v in target.items():
            if k in model:
                if type(v) == dict and type(model[k]) == dict:
                    self.merge(v, model[k])
            model[k] = v


################################################################################
# Context <-- вспомогательный класс для вызоваемых функций
################################################################################

class Context:
    # для ускорения скорости обращения
    __slots__ = ("l", "app", "backend", "event", "cmd",
                 "args", "sargs", "prefix")

    def __init__(self, app, backend=None, event={}):
        '''вспомогательный класс для вызываемых функций
        : param app     :  экземпляр Чио
        : param backend :  экземпляр движка
        : param event   :  экземпляр события'''

        self.app = app
        self.backend = backend
        self.event = event

    async def message(self, message, **params):
        '''функция отправки сообщения
        : param message  :  текст сообщения
        : param **params :  дополнительные передоваемые переметры

        : return :  API ответ'''

        # если не указана точка назначения --> id чата из события
        if 'peer_id' not in params:
            params['peer_id'] = self.event.get('to.id')
        return await self.backend.message(self.event, message, **params)

    async def request(self, method, **params):
        '''отправка API запроса
        : param method   :  названия API метода
        : param **params : дополнительно передаваемые перемтры

        : return : {"error":bool:, "response":dict}'''
        return await self.backend.request(method, **params)

    async def upload_photo(self, file=None, b=None, peer_id=None):
        '''метод загрузки фотографии
        : param file    :  путь до файла
        : param b       :  bytes переменная изображения
        : param peer_id :  точка назначения

        : return :  API ответ'''
        return await self.backend.upload_photo(file, b, peer_id)


################################################################################
# Chio <-- главный класс бота
################################################################################

class Chio:
    """Главный класс."""
    def __init__(self):
        # главные атрибуты Чио
        self.l = Logger('Core')
        self.c = Config('chiori', {'name':'Rin', 'ver':'2.3',
            'names':['/', 'Rin', 'Chio', 'Рин', 'Чио'], 'ignore_files':[]})
        self.__dict__.update(self.c.group_data)
        self.backend = None
        self.plugins = []
        self.commands = 0

        # атрибуты обработчиков
        self.handlers = {}
        self.init_handlers = []
        self.before_handlers = []
        self.after_handlers = []

    def start(self, asy=True, debug=False):
        '''метод запуска движка
        : param async :  использовать ли async/await
        : param debug :  вызов startEvent без запуска движка'''
        if debug:
            asyncio.run(self.startEvent(debug=True))

        try:
            if asy:
                asyncio.run(self.backend.start())
            else:
                self.backend.start()
        except KeyboardInterrupt:
            asyncio.run(self.stopEvent())


    # ###########################
    # events:
    # Поведение бота в определённых ситуациях.
    # ###########################


    async def startEvent(self, debug=False):
        '''метод щапуска бота
        : param debug :  выключать ли бота после успешного запуска'''
        # проверки дирректорий.
        fcheck('backends', True)
        fcheck('data', True)

        # инициализирующая прозвонка.
        await self.initCall()

        if debug:
            self.l.log('Проверка запуска завершена, семпай.', 'StartEvent')
            await self.stopEvent()

        else:
            self.l.log('Утречка, семпай.', 'StartEvent')

    async def stopEvent(self):
        # поведение бота при остановке
        self.l.log('Спокойной ночи, семпай.', 'StopEvent')
        exit()

    async def messageEvent(self, event):
        """поведение бота при получении сообщения
        : param event :  экземпляр класса события"""

        # пропускаем пустые события
        if not event:
            return True

        # получаем настройки движка, вызываем метод установки уровня
        c = Config(self.backend.name).group_data
        event = self.backend.setLevel(event, c)

        prefix = None   # <-- префикс команд
        cmd_func = None # <-- функция, которую необходимо выполнить
        cmd = ''        # <-- название обработчика, отвечающего за функцию
        args = []       # <-- список аргументов после команды
        sargs = ''      # <-- часть текста после команды

        # получаем префикс команд и удаляем его из текста
        for p in self.names:
            if event.get('text').lower().startswith(p.lower()):
                # удаляем префикс из текста события, а также удаляем пробелы
                event.model['text'] = event.get('text')[len(p):].strip()
                prefix = p
                break
        # если нету префикса мы вызываем before и after обработчики
        if not prefix:
            return await self.eventCall(event)

        # получаем список обработчиков по нашему уровню
        handlers = {}
        for k, v in self.handlers.items():
            if v['level'] <= event.get('level'):
                handlers[k] = v['func']


        # заменяем все пропуски строки на пробелы
        # разделяем сообщение по пробелам
        words = event.get('text')
        words = words.replace('\n', ' ').split(' ')

        # получаем команду из всего текста
        # цыкл while быстрее цыкла for
        while words:
            # если слово есть в словаре обработчиков
            if words[0] in handlers:
                # задаём переменные и останавливаем цыкл
                cmd_func = handlers[words[0]]
                cmd = words[0]
                words.pop(0)
                break
            # если нет --> удаляем это слово
            else:
                words.pop(0)

        # если есть команда и есть words после цыкла --> получаем аргументы
        if cmd_func and words:
            # shlex считаем аргумент в кавычках как 1 "1 2 3"
            args = shlex.split(' '.join(words).strip())
            sargs = ' '.join(words)

        # вызываем прозвонку
        return await self.eventCall(event, args, prefix, sargs, cmd_func, cmd)


    # ###########################
    # handlers:
    # добавление новых обработчиков.
    # ###########################


    def add_handler(self, cmd, func, level=1):
        '''добавить обработчик комманд
        : param cmd   :  название команды.
        : param func  :  выполняемая функция, привязанная к команде.
        : param level :  уровень доступа к команде'''

        self.handlers[cmd] = {'func':func, 'level':level}

    def add_event_handler(self, func, types=['init']):
        '''добавить обработчик события
        : param func  :  выполняемая функция, привящанная к событие
        : param types :  список типов событий,
                         для которых необходимо запускать обработчик'''

        if 'init' in types:
            self.init_handlers.append(func)
        if 'before' in types:
            self.before_handlers.append(func)
        if 'after' in types:
            self.after_handlers.append(func)

    # ###########################
    # call func:
    # функции вызова обработчиков.
    # ###########################

    async def call(self, func, *args):
        """вызов функции
        : param func :  вызываемая функция
        : param args :  передаваемые аргументы в функию"""
        await func(*args)

    async def commandCall(self, func, event, ctx):
        """вызов команды
        : param func  :  вызываемая функция
        : param event :  экземпляр события
        : param ctx   :  экземпляр контекста"""
        await self.call(func, event, ctx)

    async def initCall(self):
        # вызов init обработчиков.
        for x in self.init_handlers:
            await self.call(x, self)

    async def beforeCommandsCall(self, event, ctx):
        """вызов before обработчиков
        : param func  :  вызываемая функция
        : param event :  экземпляр события
        : param ctx   :  экземпляр контекста"""

        for x in self.before_handlers:
            await self.call(x, event, ctx)

    async def afterCommandsCall(self, event, ctx):
        """вызов after обработчиков
        : param func  :  вызываемая функция
        : param event :  экземпляр события
        : param ctx   :  экземпляр контекста"""

        for x in self.after_handlers:
            await self.call(x, event, ctx)

    async def eventCall(self, event, args=[], prefix='', sargs='', func=None,
                        cmd=''):
        ''' событие вызова прозвонки
        : param event  :  экземпляр события
        : param args   :  аргументы команды
        : param prefix :  роефик команды
        : param sargs  :  текст после команды
        : param func   :  вызываемая функция'''

        ctx = Context(self, self.backend, event)
        ctx.cmd = cmd
        ctx.args = args
        ctx.prefix = prefix
        ctx.sargs = sargs

        await self.beforeCommandsCall(event, ctx)

        if func:
            await self.commandCall(func, event, ctx)

        await self.afterCommandsCall(event, ctx)
